//go:build crypto
// +build crypto

package sandbox

import (
	"chainmaker/internal/buffer"
	"chainmaker/internal/client"
	"chainmaker/internal/mock"
	"chainmaker/logger"
	"chainmaker/pb/protogo"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"go.uber.org/zap"

	"chainmaker/sdk"
)

const (
	Version          = "Contract_SDK_Version:fc5101a7f55d71e234242163fd1bdfaa4fdea7437bf161f7e1cf7c49e57580a2"
	ExitCode_UNKNOWN = 1
	ExitCode_SIGINT  = 128 + 2
	ExitCode_SIGTERM = 128 + 15
)

var TestCase = false

type RunContractArgs struct {
	ContractNames []string
	Contracts     []sdk.Contract
}

var Logger *zap.SugaredLogger

func StartCrossMock(contractNames []string, contract []sdk.Contract) *protogo.ExecuteResult {
	//read from json file
	invokeArgs, err := readArgsFromJsonFile()
	existIfError(err)

	args := RunContractArgs{
		ContractNames: contractNames,
		Contracts:     contract,
	}

	return mockRunContract(args, invokeArgs)
}

func Start(contract sdk.Contract) error {

	args := os.Args

	if err := initConfig(); err != nil {
		return err
	}

	contractLoggerModule = generateLoggerModuleName(MODULE_CONTRACT, config.ProcessName)

	loggerBuffer := buffer.NewLoggerBuffer()
	logger := logger.NewDockerLoggerWithWriter("[Contract]", "INFO", loggerBuffer.GetWriter())
	logger.Debugf("loglevel: %s", args[4])
	logger.Debug(Version)

	// engine client
	engineRPCClient, err := newUDSClient()
	if err != nil {
		logger.Errorf("new engine client failed, err: %s", err.Error())
		return err
	}
	engineClient := newContractEngineClient(engineRPCClient, config.ProcessName, config.ContractName, logger)

	runtimeRPCClient, err := newRuntimeConn()
	if err != nil {
		logger.Errorf("new runtime client failed, err: %s", err.Error())
		return err
	}
	runtimeClient := newRuntimeClient(runtimeRPCClient, config.ContractName, logger)

	handler := newTxHandler(contract, config.ProcessName, config.ContractName, logger)

	// Register
	handler.RegisterSyscallMsgSendFunc(runtimeClient.PutMsgWithNotify)
	engineClient.RegisterTxRequestPutFunc(handler.PutMsgWithNotify)

	errCh := make(chan error, 1)
	go func() {
		errCh <- handler.Start()
	}()

	go func() {
		errCh <- runtimeClient.Start()
	}()

	go func() {
		errCh <- engineClient.Start()
	}()

	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

	logger.Infof("runtime client started, contract engine client started, tx handler started, all is well!")

	select {
	case err = <-errCh:
		logger.Errorf("sandbox stop with error, err: %s", err.Error())
	case sig := <-sigs:
		switch sig {
		case syscall.SIGTERM:
			logger.Debugf("receive signal [%d:%s], exit normally", sig, sig.String())
			logger.Debug("sandbox - end")
			os.Exit(ExitCode_SIGTERM)
		case syscall.SIGINT:
			// print tx duration and stack
			logger.Infof("receive signal [%d:%s]", sig, sig.String())
			logger.Infof("stack info: %s", GetAllStackMsg())
			logger.Debug("sandbox - end")
			os.Exit(ExitCode_SIGINT)
		default:
			logger.Infof("receive signal [%d:%s] from unknown source", sig, sig.String())
			logger.Debug("sandbox - end")
			os.Exit(ExitCode_UNKNOWN)
		}
	}

	logger.Debug("sandbox - end")
	return err
}

// mockRunContract mock run contract on local whitout chain
func mockRunContract(args RunContractArgs, invokeArgs *protogo.ContractArgs) *protogo.ExecuteResult {
	loggerBuffer := buffer.NewLoggerBuffer()
	var contractAddress string
	for address, name := range invokeArgs.AddressToNameMp {
		if name == invokeArgs.Name {
			contractAddress = address
			break
		}
	}
	_ = contractAddress
	//logger := logger.NewDockerLoggerWithWriter(fmt.Sprintf("[%s][%s]", contractAddress, invokeArgs.Name), "INFO", loggerBuffer.GetWriter())
	logger := logger.NewDockerLoggerWithWriter(fmt.Sprintf("[%s]", invokeArgs.Name), "DEBUG", loggerBuffer.GetWriter())
	start := time.Now()

	//new rpc client
	rpcClient, err := client.NewRpcClient(invokeArgs.ServerAddress)
	existIfError(err)

	contract, err := NewProxyContract(args.ContractNames, args.Contracts)

	globalStateStore, err := mock.NewGlobalStateStore(invokeArgs.GlobalSates)
	if err != nil {
		_, err = rpcClient.SendResult(newErrorResult(invokeArgs, start, err))
		existIfError(err)
	}

	sdk.InitSDKMock(invokeArgs.Name, logger, translateArgsToByte(invokeArgs.Args), globalStateStore, contract, 0, invokeArgs.AddressToNameMp)
	var invokeResponse protogo.Response
	if strings.ToUpper(invokeArgs.Method) == "INITCONTRACT" {
		invokeResponse = contract.InitContract()
	} else if strings.ToUpper(invokeArgs.Method) == "UPGRADECONTRACT" {
		invokeResponse = contract.UpgradeContract()
	} else {
		invokeResponse = contract.InvokeContract(invokeArgs.Method)
	}

	lineLogMessage, err := loggerBuffer.GetLineLogMessage()
	if err != nil {
		lineLogMessage = []byte(err.Error())
	}
	fmt.Println(string(lineLogMessage))

	result := newSuccessResult(invokeArgs, start, lineLogMessage, invokeResponse)
	//send result to server
	if !TestCase {
		_, err = rpcClient.SendResult(result)
		existIfError(err)
	}
	return result
}

func readArgsFromJsonFile() (*protogo.ContractArgs, error) {
	argsData, err := ioutil.ReadFile("args.json")
	if err != nil {
		fmt.Println("read args from file fial. " + err.Error())
		return nil, err
	}
	args := new(protogo.ContractArgs)
	err = json.Unmarshal(argsData, args)
	if err != nil {
		fmt.Println("unmarshal args fial. " + err.Error())
		return nil, err
	}
	return args, nil
}

//existIfError exist process if err not nil
func existIfError(err error) {
	if err != nil {
		fmt.Println(err.Error())
		os.Exit(-1)
	}
}

//translateEvent translate "chainmaker/pb/protogo.Event" to "generate/internal/pb/protogo.Event"
func translateEvent(events []*protogo.Event) []*protogo.ExecuteEvent {
	result := make([]*protogo.ExecuteEvent, 0)
	for i, _ := range events {
		result = append(result, &protogo.ExecuteEvent{
			Topic:        events[i].Topic,
			ContractName: events[i].ContractName,
			Data:         events[i].Data,
		})
	}
	return result
}

func translateArgsToByte(args map[string]string) map[string][]byte {
	byteArgs := make(map[string][]byte)
	for k, v := range args {
		byteArgs[k] = []byte(v)
	}
	return byteArgs
}

func translateLogsToLine(logBytes []byte) []string {
	return strings.Split(string(logBytes), "\n")
}

func newErrorResult(invokeArgs *protogo.ContractArgs, start time.Time, err error) *protogo.ExecuteResult {
	return &protogo.ExecuteResult{
		Name:        invokeArgs.Name,
		Method:      invokeArgs.Method,
		Version:     invokeArgs.Version,
		Sid:         invokeArgs.Sid,
		Seq:         invokeArgs.Seq,
		GlobalSates: nil,
		Logs:        nil,
		Event:       nil,
		TimeCost:    time.Since(start).String(),
		Response: &protogo.ExecuteResponse{
			Status:  500,
			Message: "",
			Payload: "",
		},
	}
}

func newSuccessResult(invokeArgs *protogo.ContractArgs, start time.Time, logMessage []byte, invokeResponse protogo.Response) *protogo.ExecuteResult {
	return &protogo.ExecuteResult{
		Name:        invokeArgs.Name,
		Method:      invokeArgs.Method,
		Version:     invokeArgs.Version,
		Sid:         invokeArgs.Sid,
		Seq:         invokeArgs.Seq,
		GlobalSates: sdk.Instance.(*sdk.SDK).GetStateMap().GetAll(),
		Logs:        translateLogsToLine(logMessage),
		Event:       translateEvent(sdk.Instance.(*sdk.SDK).GetEvents()),
		TimeCost:    time.Since(start).String(),
		Response: &protogo.ExecuteResponse{
			Status:  invokeResponse.Status,
			Message: invokeResponse.Message,
			Payload: string(invokeResponse.Payload),
		},
	}
}
